/*   The MIT License
 *   
 *   Carina Engine
 *   Copyright (c) 2009 2010 2011 2012 Zdravko Velinov
 *   
 *   Permission is hereby granted, free of charge, to any person obtaining a copy
 *   of this software and associated documentation files (the "Software"), to deal
 *   in the Software without restriction, including without limitation the rights
 *   to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 *   copies of the Software, and to permit persons to whom the Software is
 *   furnished to do so, subject to the following conditions:
 *
 *   The above copyright notice and this permission notice shall be included in
 *   all copies or substantial portions of the Software.
 *
 *   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 *   IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 *   FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 *   AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 *   LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 *   OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 *   THE SOFTWARE.
 */

#include "carina/lighting.hh"

namespace Carina
{
LightSourceNode::LightSourceNode(const SpatialLightSourcePtr& light_source)
    :   m_LightSource(light_source)
{

}

void LightSourceNode::draw(Scene& scene)
{
    scene.pushSpatialLightSource(shared_from_this());
}

void LightSourceNode::notifyTransform(const Matrix4& wmat)
{
    m_WorldMatrix = wmat;
}

void LightSourceNode::render(Scene& scene, const TexturePtr& albedo, const TexturePtr& normal, const TexturePtr& depth)
{
    m_LightSource->render(scene, m_WorldMatrix, albedo, normal, depth);
}

ConfigurablePtr LightSourceNode::getConfigurable()
{
    return m_LightSource;
}

ConfigurableConstPtr LightSourceNode::getConfigurable() const
{
    return m_LightSource;
}

static void UpdateMSAA(const ShaderProgramPtr& prog, bool current_msaa, bool& out_msaa, TextureVarPtr& albedo_buf, TextureVarPtr& normal_buf, TextureVarPtr& depth_buf)
{
    if(out_msaa != current_msaa)
    {
        out_msaa = current_msaa;
        auto prog_rel = prog;
        if(current_msaa)
        {
            albedo_buf = prog_rel->getTexture("AlbedoBufferMSAA");
            normal_buf = prog_rel->getTexture("NormalVectorBufferMSAA");
            depth_buf = prog_rel->getTexture("DepthBufferMSAA");
            prog_rel->useTechnique("MSAALighting");
        }
        else
        {
            albedo_buf = prog_rel->getTexture("AlbedoBuffer");
            normal_buf = prog_rel->getTexture("NormalVectorBuffer");
            depth_buf = prog_rel->getTexture("DepthBuffer");
            prog_rel->useTechnique("StandardLighting");
        }
    }
}

Spotlight::Spotlight(const ShaderProgramPtr& prog, const InputLayoutPtr& layout, const VideoBufferPtr& fsq)
    :   m_Prog(prog),
        m_Layout(layout),
        m_FullScreenQuad(fsq),
        m_uAlbedoBuffer(m_Prog->getTexture("AlbedoBuffer")),
        m_uNormalVectorBuffer(m_Prog->getTexture("NormalVectorBuffer")),
        m_uDepthBuffer(m_Prog->getTexture("DepthBuffer")),
        m_uColor(m_Prog->getVariable("Color")),
        m_uLightPosition(m_Prog->getVariable("LightPosition")),
        m_uDirection(m_Prog->getVariable("Direction")),
        m_uExponent(m_Prog->getVariable("Exponent")),
        m_uCosCutOff(m_Prog->getVariable("CosCutOff")),
        m_uAttenuation(m_Prog->getVariable("Attenuation")),
        m_uInvTransformMatrix(m_Prog->getVariable("InvTransformMatrix")),
        m_uCameraPosition(m_Prog->getVariable("CameraPosition")),
        m_MSAA(false)
{
    CE_GET_ATTRIBUTE(Color).set(0.0f, 0.0f, 0.0f);
    CE_GET_ATTRIBUTE(Attenuation).set(1.0f, 0.0f, 0.0f);
    CE_GET_ATTRIBUTE(Exponent) = 0.0f;
    CE_GET_ATTRIBUTE(CosCutOff) = 0.0f;

    m_Prog->useTechnique("StandardLighting");
}

void Spotlight::render(Scene& scene, const Matrix4& tmat, const TexturePtr& albedo, const TexturePtr& normal, const TexturePtr& depth)
{
    RendererPtr renderer = scene.getRenderer();
    CameraPtr camera = scene.getCamera();
    UpdateMSAA(m_Prog, scene.getMSAA() > 1, m_MSAA, m_uAlbedoBuffer, m_uNormalVectorBuffer, m_uDepthBuffer);
    renderer->useShaderProgram(m_Prog);
    m_uAlbedoBuffer->set(albedo);
    m_uNormalVectorBuffer->set(normal);
    m_uDepthBuffer->set(depth);
    m_uColor->set(CE_GET_ATTRIBUTE(Color));
    m_uLightPosition->set(tmat.translation());
    m_uDirection->set(tmat.relativeZ());
    m_uExponent->set(CE_GET_ATTRIBUTE(Exponent));
    m_uCosCutOff->set(CE_GET_ATTRIBUTE(CosCutOff));
    m_uAttenuation->set(CE_GET_ATTRIBUTE(Attenuation));
    m_uInvTransformMatrix->set(camera->getInvTransformMatrix());
    m_uCameraPosition->set(camera->getPosition());
    renderer->setInputLayout(m_Layout);
    renderer->setVideoBuffer(m_FullScreenQuad, sizeof(Vector3));
    renderer->draw(CE_TRIANGLE_STRIP, 0, 4);
    m_uAlbedoBuffer->reset();
    m_uNormalVectorBuffer->reset();
    m_uDepthBuffer->reset();
    m_Prog->apply();
}

PointLight::PointLight(const ShaderProgramPtr& prog, const InputLayoutPtr& layout, const VideoBufferPtr& fsq)
    :   m_Prog(prog),
        m_Layout(layout),
        m_FullScreenQuad(fsq),
        m_uAlbedoBuffer(m_Prog->getTexture("AlbedoBuffer")),
        m_uNormalVectorBuffer(m_Prog->getTexture("NormalVectorBuffer")),
        m_uDepthBuffer(m_Prog->getTexture("DepthBuffer")),
        m_uColor(m_Prog->getVariable("Color")),
        m_uLightPosition(m_Prog->getVariable("LightPosition")),
        m_uAttenuation(m_Prog->getVariable("Attenuation")),
        m_uInvTransformMatrix(m_Prog->getVariable("InvTransformMatrix")),
        m_uCameraPosition(m_Prog->getVariable("CameraPosition")),
        m_MSAA(false)
{
    CE_GET_ATTRIBUTE(Color).set(0.0f, 0.0f, 0.0f);
    CE_GET_ATTRIBUTE(Attenuation).set(1.0f, 0.0f, 0.0f);

    m_Prog->useTechnique("StandardLighting");
}

void PointLight::render(Scene& scene, const Matrix4& tmat, const TexturePtr& albedo, const TexturePtr& normal, const TexturePtr& depth)
{
    RendererPtr renderer = scene.getRenderer();
    CameraPtr camera = scene.getCamera();
    UpdateMSAA(m_Prog, scene.getMSAA() > 1, m_MSAA, m_uAlbedoBuffer, m_uNormalVectorBuffer, m_uDepthBuffer);
    renderer->useShaderProgram(m_Prog);
    m_uAlbedoBuffer->set(albedo);
    m_uNormalVectorBuffer->set(normal);
    m_uDepthBuffer->set(depth);
    m_uColor->set(CE_GET_ATTRIBUTE(Color));
    m_uLightPosition->set(tmat.translation());
    m_uAttenuation->set(CE_GET_ATTRIBUTE(Attenuation));
    m_uInvTransformMatrix->set(camera->getInvTransformMatrix());
    m_uCameraPosition->set(camera->getPosition());
    renderer->setInputLayout(m_Layout);
    renderer->setVideoBuffer(m_FullScreenQuad, sizeof(Vector3));
    renderer->draw(CE_TRIANGLE_STRIP, 0, 4);
    m_uAlbedoBuffer->reset();
    m_uNormalVectorBuffer->reset();
    m_uDepthBuffer->reset();
    m_Prog->apply();
}

DirectionalLight::DirectionalLight(const ShaderProgramPtr& prog, const InputLayoutPtr& layout, const VideoBufferPtr& fsq)
    :   m_Prog(prog),
        m_Layout(layout),
        m_FullScreenQuad(fsq),
        m_uAlbedoBuffer(m_Prog->getTexture("AlbedoBuffer")),
        m_uNormalVectorBuffer(m_Prog->getTexture("NormalVectorBuffer")),
        m_uDepthBuffer(m_Prog->getTexture("DepthBuffer")),
        m_uColor(m_Prog->getVariable("Color")),
        m_uDirection(m_Prog->getVariable("Direction")),
        m_uInvTransformMatrix(m_Prog->getVariable("InvTransformMatrix")),
        m_uCameraPosition(m_Prog->getVariable("CameraPosition")),
        m_MSAA(false)
{
    CE_GET_ATTRIBUTE(Color).set(0.0f, 0.0f, 0.0f);
    CE_GET_ATTRIBUTE(Direction).set(0.0f, 0.0f, 1.0f);

    m_Prog->useTechnique("StandardLighting");
}

void DirectionalLight::render(Scene& scene, const TexturePtr& albedo, const TexturePtr& normal, const TexturePtr& depth)
{
    RendererPtr renderer = scene.getRenderer();
    CameraPtr camera = scene.getCamera();
    UpdateMSAA(m_Prog, scene.getMSAA() > 1, m_MSAA, m_uAlbedoBuffer, m_uNormalVectorBuffer, m_uDepthBuffer);
    renderer->useShaderProgram(m_Prog);
    m_uAlbedoBuffer->set(albedo);
    m_uNormalVectorBuffer->set(normal);
    m_uDepthBuffer->set(depth);
    m_uColor->set(CE_GET_ATTRIBUTE(Color));
    m_uDirection->set(CE_GET_ATTRIBUTE(Direction));
    m_uInvTransformMatrix->set(camera->getInvTransformMatrix());
    m_uCameraPosition->set(camera->getPosition());
    renderer->setInputLayout(m_Layout);
    renderer->setVideoBuffer(m_FullScreenQuad, sizeof(Vector3));
    renderer->draw(CE_TRIANGLE_STRIP, 0, 4);
    m_uAlbedoBuffer->reset();
    m_uNormalVectorBuffer->reset();
    m_uDepthBuffer->reset();
    m_Prog->apply();
}

AmbientLight::AmbientLight(const ShaderProgramPtr& prog, const InputLayoutPtr& layout, const VideoBufferPtr& fsq)
    :   m_Prog(prog),
        m_Layout(layout),
        m_FullScreenQuad(fsq),
        m_uAlbedoBuffer(m_Prog->getTexture("AlbedoBuffer")),
        m_uColor(m_Prog->getVariable("Color")),
        m_MSAA(false)
{
    CE_GET_ATTRIBUTE(Color).set(0.0f, 0.0f, 0.0f);

    m_Prog->useTechnique("StandardLighting");
}

void AmbientLight::render(Scene& scene, const TexturePtr& albedo, const TexturePtr& normal, const TexturePtr& depth)
{
    RendererPtr renderer = scene.getRenderer();
    CameraPtr camera = scene.getCamera();
    bool current_msaa = scene.getMSAA() > 1;
    if(m_MSAA != current_msaa)
    {
        m_MSAA = current_msaa;
        if(current_msaa)
        {
            m_uAlbedoBuffer = m_Prog->getTexture("AlbedoBufferMSAA");
            m_Prog->useTechnique("MSAALighting");
        }
        else
        {
            m_uAlbedoBuffer = m_Prog->getTexture("AlbedoBuffer");
            m_Prog->useTechnique("StandardLighting");
        }
    }
    renderer->useShaderProgram(m_Prog);
    m_uAlbedoBuffer->set(albedo);
    m_uColor->set(CE_GET_ATTRIBUTE(Color));
    renderer->setInputLayout(m_Layout);
    renderer->setVideoBuffer(m_FullScreenQuad, sizeof(Vector3));
    renderer->draw(CE_TRIANGLE_STRIP, 0, 4);
    m_uAlbedoBuffer->reset();
    m_Prog->apply();
}

SpotlightFactory::SpotlightFactory(const ResourceManagerPtr& res_mgr)
{
    RendererPtr renderer = res_mgr->getRenderer();
    
    Vector3 fullscreen_quad[] =
    {
        Vector3(-1.0f, -1.0f, 0.0f),
        Vector3(-1.0f,  1.0f, 0.0f),
        Vector3( 1.0f, -1.0f, 0.0f),
        Vector3( 1.0f,  1.0f, 0.0f)
    };

    InputElementArray arr(1);
    arr[0].set("POSITION", VT_RGB32F, 0);
    m_Prog = res_mgr->getShaderProgram("spotlight");
    m_Layout = renderer->createInputLayout(m_Prog, arr);
    m_FullScreenQuad = renderer->createVideoBuffer(CE_VERTEX_BUFFER, sizeof(fullscreen_quad), fullscreen_quad, CE_STATIC_DRAW);
}

LightSourcePtr SpotlightFactory::create()
{
    return make_aligned_shared<Spotlight>(m_Prog, m_Layout, m_FullScreenQuad);
}

PointLightFactory::PointLightFactory(const ResourceManagerPtr& res_mgr)
{
    RendererPtr renderer = res_mgr->getRenderer();
    
    Vector3 fullscreen_quad[] =
    {
        Vector3(-1.0f, -1.0f, 0.0f),
        Vector3(-1.0f,  1.0f, 0.0f),
        Vector3( 1.0f, -1.0f, 0.0f),
        Vector3( 1.0f,  1.0f, 0.0f)
    };

    InputElementArray arr(1);
    arr[0].set("POSITION", VT_RGB32F, 0);
    m_Prog = res_mgr->getShaderProgram("point_light");
    m_Layout = renderer->createInputLayout(m_Prog, arr);
    m_FullScreenQuad = renderer->createVideoBuffer(CE_VERTEX_BUFFER, sizeof(fullscreen_quad), fullscreen_quad, CE_STATIC_DRAW);
}

LightSourcePtr PointLightFactory::create()
{
    return make_aligned_shared<PointLight>(m_Prog, m_Layout, m_FullScreenQuad);
}

DirectionalLightFactory::DirectionalLightFactory(const ResourceManagerPtr& res_mgr)
{
    RendererPtr renderer = res_mgr->getRenderer();
    
    Vector3 fullscreen_quad[] =
    {
        Vector3(-1.0f, -1.0f, 0.0f),
        Vector3(-1.0f,  1.0f, 0.0f),
        Vector3( 1.0f, -1.0f, 0.0f),
        Vector3( 1.0f,  1.0f, 0.0f)
    };

    InputElementArray arr(1);
    arr[0].set("POSITION", VT_RGB32F, 0);
    m_Prog = res_mgr->getShaderProgram("directional_light");
    m_Layout = renderer->createInputLayout(m_Prog, arr);
    m_FullScreenQuad = renderer->createVideoBuffer(CE_VERTEX_BUFFER, sizeof(fullscreen_quad), fullscreen_quad, CE_STATIC_DRAW);
}

LightSourcePtr DirectionalLightFactory::create()
{
    return make_aligned_shared<DirectionalLight>(m_Prog, m_Layout, m_FullScreenQuad);
}

AmbientLightFactory::AmbientLightFactory(const ResourceManagerPtr& res_mgr)
{
    RendererPtr renderer = res_mgr->getRenderer();
    
    Vector3 fullscreen_quad[] =
    {
        Vector3(-1.0f, -1.0f, 0.0f),
        Vector3(-1.0f,  1.0f, 0.0f),
        Vector3( 1.0f, -1.0f, 0.0f),
        Vector3( 1.0f,  1.0f, 0.0f)
    };

    InputElementArray arr(1);
    arr[0].set("POSITION", VT_RGB32F, 0);
    m_Prog = res_mgr->getShaderProgram("ambient_light");
    m_Layout = renderer->createInputLayout(m_Prog, arr);
    m_FullScreenQuad = renderer->createVideoBuffer(CE_VERTEX_BUFFER, sizeof(fullscreen_quad), fullscreen_quad, CE_STATIC_DRAW);
}

LightSourcePtr AmbientLightFactory::create()
{
    return make_aligned_shared<AmbientLight>(m_Prog, m_Layout, m_FullScreenQuad);
}

LightingManager::LightingManager(const ResourceManagerPtr& res_mgr)
{
    registerFactory("Spotlight", make_aligned_shared<SpotlightFactory>(res_mgr));
    registerFactory("Directional light", make_aligned_shared<DirectionalLightFactory>(res_mgr));
    registerFactory("Point light", make_aligned_shared<PointLightFactory>(res_mgr));
    registerFactory("Ambient light", make_aligned_shared<AmbientLightFactory>(res_mgr));
}
}
